/*
 * Copyright 2015 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.agerasample.testapp;

import static com.google.agerasample.testapp.Note.note;
import static com.google.agerasample.testapp.NoteGroup.noteGroup;
import static com.google.agerasample.testapp.NotesSqlDatabaseSupplier.*;
import static com.google.agera.Functions.staticFunction;
import static com.google.agera.Observables.updateDispatcher;
import static com.google.agera.Repositories.repositoryWithInitialValue;
import static com.google.agera.RepositoryConfig.SEND_INTERRUPT;
import static com.google.agera.database.SqlDatabaseFunctions.databaseDeleteFunction;
import static com.google.agera.database.SqlDatabaseFunctions.databaseInsertFunction;
import static com.google.agera.database.SqlDatabaseFunctions.databaseQueryFunction;
import static com.google.agera.database.SqlDatabaseFunctions.databaseUpdateFunction;
import static com.google.agera.database.SqlRequests.sqlDeleteRequest;
import static com.google.agera.database.SqlRequests.sqlInsertRequest;
import static com.google.agera.database.SqlRequests.sqlRequest;
import static com.google.agera.database.SqlRequests.sqlUpdateRequest;

import static java.lang.String.valueOf;
import static java.util.Collections.emptyList;
import static java.util.concurrent.Executors.newSingleThreadExecutor;

import com.google.agera.*;
import com.google.agera.database.*;

import ohos.app.Context;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.Executor;

/**
 * Encapsulates all database interactions for {@link Note}s. Getting a list of all notes is
 * implemented with a {@link #getNotesRepository()} Repository} that can be activated by and
 * observed from the Activity.
 *
 * @since 2020-12-18
 */
public final class NotesStore {
    private static final String MODIFY_NOTE_WHERE = NOTES_NOTE_ID_COLUMN + "=?";
    private static final String GET_NOTES_FROM_TABLE =
            "SELECT "
                    + NOTES_NOTE_ID_COLUMN
                    + ", "
                    + NOTES_NOTE_COLUMN
                    + " FROM "
                    + NOTES_TABLE
                    + " ORDER BY "
                    + NOTES_NOTE_COLUMN;
    private static final int ID_COLUMN_INDEX = 0;
    private static final int NOTE_COLUMN_INDEX = 1;
    private static final List<NoteGroup> INITIAL_VALUE = emptyList();

    private static final Executor STORE_EXECUTOR = newSingleThreadExecutor();

    private final Repository<List<NoteGroup>> notesRepository;

    private final Receiver<SqlInsertRequest> insert;

    private final Receiver<SqlUpdateRequest> update;

    private final Receiver<SqlDeleteRequest> delete;

    private final SqlDatabaseSupplier databaseSupplier;

    private NotesStore(
            final Repository<List<NoteGroup>> notesRepository,
            final Receiver<SqlInsertRequest> insert,
            final Receiver<SqlUpdateRequest> update,
            final Receiver<SqlDeleteRequest> delete,
            final SqlDatabaseSupplier databaseSupplier) {
        this.insert = insert;
        this.update = update;
        this.delete = delete;
        this.notesRepository = notesRepository;
        this.databaseSupplier = databaseSupplier;
    }

    public static synchronized NotesStore notesStore(final Context applicationContext) {
        // Create a database supplier that initializes the database. This is also used to supply the
        // database in all database operations.
        final NotesSqlDatabaseSupplier databaseSupplier = databaseSupplier(applicationContext);

        // Create a function that processes database write operations.
        final Function<SqlInsertRequest, Result<Long>> insertNoteFunction = databaseInsertFunction(databaseSupplier);
        final Function<SqlUpdateRequest, Result<Integer>> updateNoteFunction = databaseUpdateFunction(databaseSupplier);
        final Function<SqlDeleteRequest, Result<Integer>> deleteNoteFunction = databaseDeleteFunction(databaseSupplier);

        final UpdateDispatcher updateDispatcher = updateDispatcher();

        final Receiver<SqlDeleteRequest> delete =
                value ->
                        STORE_EXECUTOR.execute(
                                () -> {
                                    deleteNoteFunction.apply(value);
                                    updateDispatcher.update();
                                });

        final Receiver<SqlUpdateRequest> update =
                value ->
                        STORE_EXECUTOR.execute(
                                () -> {
                                    updateNoteFunction.apply(value);
                                    updateDispatcher.update();
                                });

        final Receiver<SqlInsertRequest> insert =
                new Receiver<SqlInsertRequest>() {
                    @Override
                    public void accept(SqlInsertRequest value) {
                        STORE_EXECUTOR.execute(
                                new Runnable() {
                                    @Override
                                    public void run() {
                                        insertNoteFunction.apply(value);
                                        updateDispatcher.update();
                                    }
                                });
                    }
                };

        // Create the wired up notes store
        return new NotesStore(
                repositoryWithInitialValue(INITIAL_VALUE)
                        .observe(updateDispatcher)
                        .onUpdatesPerLoop()
                        .goTo(STORE_EXECUTOR)
                        .getFrom(() -> sqlRequest().sql(GET_NOTES_FROM_TABLE).compile())
                        .attemptTransform(
                                databaseQueryFunction(
                                        databaseSupplier,
                                        cursor ->
                                                note(
                                                        cursor.getInt(ID_COLUMN_INDEX),
                                                        cursor.getString(NOTE_COLUMN_INDEX))))
                        .orEnd(staticFunction(INITIAL_VALUE))
                        .thenTransform(
                                notes -> {
                                    final Map<Character, List<Note>> notesGroupsData = new TreeMap<>();
                                    for (final Note note : notes) {
                                        final String noteText = note.getNote();

                                        final char groupId =
                                                (null == noteText || "".equals(noteText)) ? 0 : noteText.charAt(0);
                                        List<Note> notesGroupData = notesGroupsData.get(groupId);
                                        if (notesGroupData == null) {
                                            notesGroupData = new ArrayList<>();
                                            notesGroupsData.put(groupId, notesGroupData);
                                        }
                                        notesGroupData.add(note);
                                    }
                                    final List<NoteGroup> notesGroups = new ArrayList<>();
                                    for (final Map.Entry<Character, List<Note>> groupData :
                                            notesGroupsData.entrySet()) {
                                        notesGroups.add(
                                                noteGroup(
                                                        String.valueOf(groupData.getKey()),
                                                        groupData.getKey(),
                                                        groupData.getValue()));
                                    }
                                    return notesGroups;
                                })
                        .onConcurrentUpdate(SEND_INTERRUPT)
                        .onDeactivation(SEND_INTERRUPT)
                        .compile(),
                insert,
                update,
                delete,
                databaseSupplier);
    }

    public Repository<List<NoteGroup>> getNotesRepository() {
        return notesRepository;
    }

    public void insertNoteFromText(final String noteText) {
        insert.accept(sqlInsertRequest().table(NOTES_TABLE).column(NOTES_NOTE_COLUMN, noteText).compile());
    }

    public void deleteNote(final Note note) {
        delete.accept(
                sqlDeleteRequest()
                        .table(NOTES_TABLE)
                        .where(MODIFY_NOTE_WHERE)
                        .arguments(valueOf(note.getId()))
                        .compile());
    }

    public void updateNote(final Note note, final String noteText) {
        update.accept(
                sqlUpdateRequest()
                        .table(NOTES_TABLE)
                        .column(NOTES_NOTE_COLUMN, noteText)
                        .where(MODIFY_NOTE_WHERE)
                        .arguments(valueOf(note.getId()))
                        .compile());
    }

    public void clearNotes() {
        delete.accept(sqlDeleteRequest().table(NOTES_TABLE).compile());
    }

    public void closeDatabase() {
        databaseSupplier.get().get().close();
    }
}
